using System;
using System.Data;
using System.Collections.Generic;
using Microsoft.Practices.ServiceLocation;

namespace Caramel
{
	public static class UnitOfWork
	{
		private const string CurrentUnitOfWorkStackKey = "__CurrentUnitOfWorkStack__";

		public static Func<IUnitOfWorkFactory> GetCurrentUnitOfWorkFactory = () => GetCurrentUnitOfWorkFactoryInstance();

		public static IUnitOfWork Begin()
		{
			return UnitOfWork.Begin(null, IsolationLevel.ReadCommitted);
		}

		public static IUnitOfWork Begin(IsolationLevel isolationLevel)
		{
			return UnitOfWork.Begin(null, isolationLevel);
		}

		public static IUnitOfWork Begin(string name)
		{
			UnitOfWork.Current =
				UnitOfWork.
					GetCurrentUnitOfWorkFactory().
					Create(name, IsolationLevel.ReadCommitted);

			return UnitOfWork.Current;
		}

		public static IUnitOfWork Begin(string name, IsolationLevel isolationLevel)
		{
			UnitOfWork.Current =
				UnitOfWork.
					GetCurrentUnitOfWorkFactory().
					Create(name, isolationLevel);

			return UnitOfWork.Current;
		}

		public static IUnitOfWork BeginReadOnly()
		{
			return UnitOfWork.BeginReadOnly(null, IsolationLevel.ReadCommitted);
		}

		public static IUnitOfWork BeginReadOnly(IsolationLevel isolationLevel)
		{
			return UnitOfWork.BeginReadOnly(null, isolationLevel);
		}

		public static IUnitOfWork BeginReadOnly(string name)
		{
			UnitOfWork.Current =
				UnitOfWork.
					GetCurrentUnitOfWorkFactory().
					CreateReadOnly(name, IsolationLevel.ReadCommitted);

			return UnitOfWork.Current;
		}

		public static IUnitOfWork BeginReadOnly(string name, IsolationLevel isolationLevel)
		{
			UnitOfWork.Current =
				UnitOfWork.
					GetCurrentUnitOfWorkFactory().
					CreateReadOnly(name, isolationLevel);

			return UnitOfWork.Current;
		}

		public static IUnitOfWork Current
		{
			get
			{
				var stack = LocalStorage.Current[UnitOfWork.CurrentUnitOfWorkStackKey] as Stack<UnitOfWorkTracker>;

				if ((stack == null ) || (stack.Count == 0))
				{
					return null;
				}

				return stack.Peek().UnitOfWork;
			}
			private set
			{
				var stack = LocalStorage.Current[UnitOfWork.CurrentUnitOfWorkStackKey] as Stack<UnitOfWorkTracker>;

				if (stack == null)
				{
					stack = new Stack<UnitOfWorkTracker>();
				}

				stack.Push(new UnitOfWorkTracker() { UnitOfWork = value });
				LocalStorage.Current[UnitOfWork.CurrentUnitOfWorkStackKey] = stack;
			}
		}

		public static void DisposeOfUnitOfWork(IUnitOfWork work)
		{
			var stack = LocalStorage.Current[UnitOfWork.CurrentUnitOfWorkStackKey] as Stack<UnitOfWorkTracker>;

			if (stack != null)
			{
				var popped = stack.Pop();

				if (Object.ReferenceEquals(popped, work))
				{
					// TODO: Confirm popped equals work - blow up??
				}
			}
		}

		public static void ReadOnly(Action action)
		{
			UnitOfWork.ReadOnly(work => action());
		}

		public static void ReadOnly(Action<IUnitOfWork> action)
		{
			using (var work = UnitOfWork.BeginReadOnly())
			{
				try
				{
					action(work);
				}
				catch
				{
					throw;
				}
			}
		}

		public static TReturn ReadOnly<TReturn>(Func<TReturn> action)
		{
			return UnitOfWork.ReadOnly<TReturn>(work => action());
		}

		public static TReturn ReadOnly<TReturn>(Func<IUnitOfWork, TReturn> action)
		{
			using (var work = UnitOfWork.BeginReadOnly())
			{
				try
				{
					return action(work);
				}
				catch
				{
					throw;
				}
			}
		}

		public static void Commit(Action action)
		{
			UnitOfWork.Commit(work => action());
		}

		public static void Commit(Action<IUnitOfWork> action)
		{
			using (var work = UnitOfWork.Begin())
			{
				try
				{
					action(work);
					work.Commit();
				}
				catch
				{
					work.Rollback();
					throw;
				}
			}
		}

		public static TReturn Commit<TReturn>(Func<TReturn> action)
		{
			return UnitOfWork.Commit<TReturn>(work => action());
		}

		public static TReturn Commit<TReturn>(Func<IUnitOfWork, TReturn> action)
		{
			using (var work = UnitOfWork.Begin())
			{
				try
				{
					var value = action(work);
					work.Commit();

					return value;
				}
				catch
				{
					work.Rollback();
					throw;
				}
			}
		}

		private static IUnitOfWorkFactory GetCurrentUnitOfWorkFactoryInstance()
		{
			//if (ServiceLocator.Current == null)
			//{
			//    throw new NullReferenceException("The current ambient container is not set to an instance of an object.");
			//}

			return ServiceLocator.Current.GetInstance<IUnitOfWorkFactory>();
		}

		private class UnitOfWorkTracker
		{
			private int _usageCount = 1;

			public IUnitOfWork UnitOfWork { get; set; }

			public int UsageCount { get { return _usageCount; } }

			public void IncrementUsageCount()
			{
				_usageCount++;
			}

			public void DecrementUsageCount()
			{
				_usageCount--;
			}
		}
	}
}
